Android 开发中可能会导致内存泄露的问题

2015-06-15 17:13

作者:给立乐*
出处:http://spencer-dev.com/2015/06/15/Android 开发中可能会导致内存泄露的问题
声明:本文采用以下协议进行授权: 自由转载-非商用-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处。

在android编码中,会有一些简便的写法和编码习惯,会导致我们的代码有很多内存泄露的问题。

在这里做一个已知错误的总结(其中有一些是个人总结和参考其他博主的文章,在此表示感谢)。

本文会不定时更新,将自己遇到的内存泄漏相关的问题记录下来并提供解决办法。

1,编写单例的时候常出现的错误。

错误方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Foo{
private static Foo foo;
private Context mContext;
private Foo(Context mContext){
this.mContext = mContext;
}
// 普通单例,非线程安全
public static Foo getInstance(Context mContext){
if(foo == null)
foo = new Foo(mContext);
return foo;
}
public void otherAction(){
mContext.xxxx();
....
}
}

错误原因:
如果我们在 Activity 中或者其他地方使用 Foo.getInstance() 时,我们总是会顺手写一个 this 或者 mContext(这个变量也是指向 this)。试想一下,当前我们所用的 Foo 是单例,意味着被初始化后会一直存在与内存中,以方便我们以后调用的时候不会在此次创建 Foo 对象。但 Foo 中的 mContext 变量一直都会持有 Activity 中的 mContext,导致 Activity 即使执行了 onDestroy 方法,也不能够将自己销毁。但 ApplicationContext 就不同了,它一直伴随着我们应用存在(中途也可能会被销毁,但也会自动 reCreate),所以就不用担心 Foo 中的 mContext 会持有某 Activity 的引用,让其无法销毁。

正确方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Foo{
private static Foo foo;
private Context mContext;
private Foo(Context mContext){
this.mContext = mContext;
}
// 普通单例,非线程安全
public static Foo getInstance(Context mContext){
if(foo == null)
foo = new Foo(mContext.getApplicationContext());
return foo;
}
public void otherAction(){
mContext.xxxx();
....
}
}


2,使用匿名内部类的时候经常出现的错误

错误方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class FooActivity extends Activity{
private TextView textView;
private Handler handler = new Handler(){
@override
public void handlerMessage(Message msg){
}
};
@override
public void onCreate(Bundle bundle){
super.onCreate(bundle);
setContextView(R.layout.activity_foo_layout);
textView = (TextView)findViewById(R.id.textView);
handler.postDelayed(new Runnable(){
@override
public void run(){
textView.setText(“ok”);
};
},1000 * 60 * 10);
}
}

错误原因:
当我们执行了 FooActivity 的 finish 方法,被延迟的消息会在被处理之前存在于主线程消息队列中10分钟,而这个消息中又包含了 Handler 的引用,而 Handler 是一个匿名内部类的实例,其持有外面的 FooActivity 的引用,所以这导致了 FooActivity 无法回收,进而导致 FooActivity 持有的很多资源都无法回收,所以产生了内存泄露。

注意上面的 new Runnable 这里也是匿名内部类实现的,同样也会持有 FooActivity 的引用,也会阻止 FooActivity 被回收。

一个静态的匿名内部类实例不会持有外部类的引用。

正确方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class FooActivity extends Activity{
private TextView textView;
private static class MyHandler extends Handler {
private final WeakReference<FooActivity> mActivity;
public MyHandler(FooActivity activity) {
mActivity = new WeakReference<FooActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
FooActivity activity = mActivity.get();
if (activity != null) {
....
}
}
}
private final MyHandler handler = new MyHandler(this);
@override
public void onCreate(Bundle bundle){
super.onCreate(bundle);
setContextView(R.layout.activity_foo_layout);
textView = (TextView)findViewById(R.id.textView);
handler.postDelayed(new MyRunnable(textView),1000 * 60 * 10);
}
private static class MyRunnable implements Runnable{
private WeakReference<TextView> textViewWeakReference;
public MyRunnable(TextView textView){
textViewWeakReference = new WeakReference<TextView>(textView);
}
@override
public void run(){
final TextView textView = textViewWeakReference.get();
if(textView != null){
textView.setText("OK");
}
};
}
}


3,关于 Handler 的使用

使用 Handler 后,记得在 onDestroy 里面调用 handler.removeCallbacksAndMessages(object token);

1
2
3
4
5
6
7
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
// removeCallbacksAndMessages,当参数为null的时候,可以清除掉所有跟当前handler相关的Runnable和Message。
// 我们在onDestroy中调用次方法也就不会发生内存泄漏了。
}

4,InputMethodManager内存泄露现象

现象为:某个界面上出现 InputMethodManager 持有一 Activity,导致该 Activity 无法回收。如果该 Activity 再次被打开,则旧的会释放掉,但新打开的会被继续持有无法释放回收。

此问题为部分系统问题。现已知的有:

1
2
3
三星Note3 N9008 官方ROM 4.4.2
天语k_touch_v9 官方ROM 4.0.4

这里只是给出了解决方法,具体的解决思路请参考下面:
[Android][Memory Leak] InputMethodManager内存泄露现象及解决

使用如下代码在 Activity 的 onDestroy 中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void fixInputMethodManagerLeak(Context destContext) {
if (destContext == null) {
return;
}
InputMethodManager imm = (InputMethodManager) destContext.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) {
return;
}
String [] arr = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
Field f = null;
Object obj_get = null;
for (int i = 0;i < arr.length;i ++) {
String param = arr[i];
try{
f = imm.getClass().getDeclaredField(param);
if (f.isAccessible() == false) {
f.setAccessible(true);
} // author: sodino mail:sodino@qq.com
obj_get = f.get(imm);
if (obj_get != null && obj_get instanceof View) {
View v_get = (View) obj_get;
if (v_get.getContext() == destContext) { // 被InputMethodManager持有引用的context是想要目标销毁的
f.set(imm, null); // 置空,破坏掉path to gc节点
} else { // 不是想要目标销毁的,即为又进了另一层界面了,不要处理,避免影响原逻辑,也就不用继续for循环了
}
break;
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
}

开发中需要注意的点以免内存泄漏:

1,不要让生命周期长于 Activity 的对象持有到 Activity 的引用。

2,尽量使用 Application 的 Context 而不是 Activity 的 Context。

3,尽量不要在 Activity 中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用(具体可以查看细话Java:”失效”的private修饰符了解)。如果使用静态内部类,将外部实例引用作为弱引用持有。

4,垃圾回收不能解决内存泄露,了解Android中垃圾回收机制


获取 Context 的方法,以及使用上 Context 和 ApplicationContext 的区别:

1,View.getContext,返回当前 View 对象的 Context 对象,通常是当前正在展示的Activity对象。

2,Activity.getApplicationContext,获取当前 Activity 所在的(应用)进程的 Context 对象,通常我们使用 Context 对象时,要优先考虑这个全局的进程 Context。

3,ContextWrapper.getBaseContext() 用来获取一个 ContextWrapper 进行装饰之前的 Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。

4,Activity.this 返回当前的 Activity 实例,如果是 UI 控件需要使用 Activity 作为 Context 对象,但是默认的 Toast 实际上使用 ApplicationContext 也可以。

image

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是 YES,但是为什么说是 NO 呢?下面一个一个解释:

数字1:启动 Activity 在这些类中是可以的,但是需要创建一个新的 task。一般情况不推荐。

数字2:在这些类中去 layout inflate 是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在 receiver 为 null 时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver 之所以在上述表格中,是因为在其内部方法中都有一个 Context 用于使用。

好了,这里我们看下表格,重点看 Activity 和 Application,可以看到,和 UI 相关的方法基本都不建议或者不可使用 Application,并且前三个操作基本不可能在 Application 中出现。实际上只要把握住一点,凡是跟 UI 相关的,都应该使用 Activity 做为 Context 来处理。其他的一些操作,Service / Activity / Application 等实例都可以。当然了,注意 Context 引用的持有,防止内存泄漏。


本文参考:

Android Weak Handler:可以避免内存泄漏的 Handler 库

Android中Handler引起的内存泄露

细话Java:”失效”的private修饰符

避免 Android 中 Context 引起的内存泄露

Android Context 上下文 你必须知道的一切

[Android][Memory Leak] InputMethodManager内存泄露现象及解决


Comments: